package com.airport.schedule.genetic;

import java.util.*;
import java.util.function.Function;

public class Genotype {
    public double secondValue;

    private static Random random = new Random(System.currentTimeMillis());
    private final int[][] validGeneAlleles;
    private final int popSize;

    private List<int[]> population;
    private double mutateRate;
    private double crossRate;
    private double offspringFrac = 0.9;
    private int[] bestChromosome;
    private double fittestValue = Double.MAX_VALUE;
    private Function<int[], Double> evaluator;
    private boolean minimizer = true;
    private Map<int[], Double> fitnessMap;
    private int K = 3;

    public Genotype(int popSize, double mutateRate, double crossRate,
                    int[][] validGeneAlleles, Function<int[], Double> evaluator) {
        this.popSize = popSize;
        this.validGeneAlleles = validGeneAlleles;
        this.population = new ArrayList<>();
        this.evaluator = evaluator;
        this.mutateRate = mutateRate;
        this.crossRate = crossRate;
    }

    public Genotype(int popSize, double mutateRate, double crossRate, double offspringFrac,
                    int[][] validGeneAlleles,
                    List<int[]> chromosomes, Function<int[], Double> evaluator) {
        this.validGeneAlleles = validGeneAlleles;
        this.evaluator = evaluator;
        this.mutateRate = mutateRate;
        this.crossRate = crossRate;
        this.popSize = popSize;
        this.population = chromosomes;
        this.offspringFrac = offspringFrac;
    }

    public void keepConstantPopSize() {
        while(population.size() < popSize) {
            int[] chromosome = new int[validGeneAlleles.length];
            for(int j = 0; j < chromosome.length; j++) {
                int size = validGeneAlleles[j].length;
                chromosome[j] = validGeneAlleles[j][random.nextInt(size)];
            }
            population.add(chromosome);
        }
        fitnessMap = new HashMap<>();
        for(int[] chromosome : population) {
            fitnessMap.put(chromosome, evaluator.apply(chromosome));
        }
        if(minimizer) {
            Collections.sort(population, (i, j) -> Double.compare(fitnessMap.get(i), fitnessMap.get(j)));
        }else{
            Collections.sort(population, (i, j) -> Double.compare(fitnessMap.get(j), fitnessMap.get(i)));
        }
        bestChromosome = population.get(0);
        fittestValue = fitnessMap.get(bestChromosome);
        while (population.size() > popSize) {
            double max = -1;
            int devil = -1;
            for(int k = 0; k < K; k++) {
                int i = random.nextInt(population.size());
                double val = fitnessMap.get(population.get(i));
                if(max < val) {
                    max = val;
                    devil = i;
                }
            }
            population.remove(devil);
        }
        secondValue = fitnessMap.get(population.get(1));
    }

    private int select() {
        double min = Double.MAX_VALUE;
        int best = -1;
        for(int k = 0; k < K; k++) {
            int i = random.nextInt(population.size());
            double val = fitnessMap.get(population.get(i));
            if(min > val) {
                min = val;
                best = i;
            }
        }
        return best;
    }

    public double getFittestValue() {
        return fittestValue;
    }

    public int[] getBestChromosome() {
        return bestChromosome;
    }

    public void evolve() {
        keepConstantPopSize();
        // crossover ................................
        List<int[]> newPopulation = new ArrayList<>(popSize * 2);
        int preGenSize = popSize - (int)(popSize * offspringFrac);
        newPopulation.addAll(population.subList(0, preGenSize));
        int crossNum = (int)(popSize * crossRate);
        for(int i = 0; i < crossNum; i++) {
            int[] c1 = population.get(select());
            int[] c2 = population.get(select());
            if(c1 != c2) {
                int[] offspring1 = new int[c1.length];
                int[] offspring2 = new int[c1.length];
                System.arraycopy(c1, 0, offspring1, 0, c1.length);
                System.arraycopy(c2, 0, offspring2, 0, c2.length);
                crossover(offspring1, offspring2);
                newPopulation.add(offspring1);
                newPopulation.add(offspring2);
            }
        }
        // mutating..................................
        for(int i = 0; i < popSize; i++) {
            double prob = random.nextDouble();
            if(mutateRate <= prob) {
                int[] c = population.get(i);
                int[] offspring = new int[c.length];
                System.arraycopy(c, 0, offspring, 0, c.length);
                mutate(offspring, prob);
                newPopulation.add(offspring);
            }
        }
        population = newPopulation;
    }

    public void setMaximizer() {
        this.minimizer = false;
    }

    private void mutate(int[] offspring, double prob) {
        for(int i = 0; i < offspring.length; i++) {
            if(random.nextDouble() <= prob) {
                int size = validGeneAlleles[i].length;
                offspring[i] = validGeneAlleles[i][random.nextInt(size)];
            }
        }
    }

    private void crossover(int[] offspring1, int[] offspring2) {
        int p1 = random.nextInt(offspring1.length);
        int p2 = random.nextInt(offspring1.length);
        while(p1 == p2) p2 = random.nextInt(offspring1.length);
        if(p1 < p2) {
            for(int i = p1; i <= p2; i++) {
                int allele = offspring1[i];
                offspring1[i] = offspring2[i];
                offspring2[i] = allele;
            }
        }else{
            for(int i = p1; i < offspring1.length; i++) {
                int allele = offspring1[i];
                offspring1[i] = offspring2[i];
                offspring2[i] = allele;
            }
            for(int i = 0; i <= p2; i++) {
                int allele = offspring1[i];
                offspring1[i] = offspring2[i];
                offspring2[i] = allele;
            }
        }
    }

}
